4.1 定义

函数是结构化编程的最小模块单元。它将复杂的算法过程分解为若干较小任务,隐藏相关细节,使得程序结构更加清晰,易于维护。函数被设计成相对独立,通过接收输入参数完成一段算法指令,输出或存储相关结果。因此,函数还是代码复用和测试的基本单元。

关键字func用于定义函数。Go中的函数有些不太方便的限制,但也借鉴了动态语言的某些优点。

  • 无须前置声明。
  • 不支持命名嵌套定义(nested)。
  • 不支持同名函数重载(overload)。
  • 不支持默认参数。
  • 支持不定长变参。
  • 支持多返回值。
  • 支持命名返回值。
  • 支持匿名函数和闭包。

和前面曾说过的一样,左花括号不能另起一行

func test()            
{             // 错误:syntax error:unexpected semicolon or newline before{ 
} 
  
func test(x int) {      // 错误:test redeclared in this block
} 
  
func main() { 
   func add(x,y int)int{  // 错误:syntax error:unexpected add,expecting( 
       return x+y
    } 
}

函数属于第一类对象,具备相同签名(参数及返回值列表)的视作同一类型。

func hello() { 
   println("hello,world!") 
} 
  
func exec(f func()) { 
   f() 
} 
  
func main() { 
   f:=hello
   exec(f) 
}

第一类对象(first-class object)指可在运行期创建,可用作函数参数或返回值,可存入变量的实体。最常见的用法就是匿名函数。

从阅读和代码维护的角度来说,使用命名类型更加方便。

// 定义函数类型 
type FormatFunc func(string, ...interface{}) (string,error) 
  
// 如不使用命名类型,这个参数签名会长到没法看 
func format(f FormatFunc,s string,a...interface{}) (string,error) { 
   return f(s,a...) 
}

函数只能判断其是否为nil,不支持其他比较操作。

func a() {}
func b() {}
func main() {
   println(a==nil)
   println(a==b)                     // 无效操作:a==b(func can only be compared to nil)
}
 

从函数返回局部变量指针是安全的,编译器会通过逃逸分析(escape analysis)来决定是否在堆上分配内存。

func test() *int{ 
   a:=0x100
   return &a
} 
  
func main() { 
   var a*int=test() 
   println(a, *a) 
}

输出:

$go build-gcflags"-l-m"       // 禁用函数内联,输出优化信息 
  
moved to heap:a
&a escapes to heap
  
  
$go tool objdump-s"main\.main"test// 反汇编确认 
  
TEXT main.main(SB)test.go
   CALL main.test(SB) 
  
  
$ ./test
0xc820074000 256

函数内联(inline)对内存分配有一定的影响。如果上例中允许内联,那么就会直接在栈上分配内存。

$go build-gcflags"-m"     // 默认优化方式,允许内联 
  
inlining call to test
main&a does not escape
  
  
$go tool objdump-s"main\.main"test
  
TEXT main.main(SB)test.go
   MOVQ$0x100,0x10(SP) 
   LEAQ 0x10(SP),BX
   MOVQ BX,0x18(SP) 
   MOVQ 0x18(SP),BX
   MOVQ BX,0(SP) 
   CALL runtime.printpointer(SB)

当前编译器并未实现尾递归优化(tail-call optimization)。尽管Go执行栈的上限是GB规模,轻易不会出现堆栈溢出(stack overflow)错误,但依然需要注意拷贝栈的复制成本。

内存管理相关内容,请阅读本书下卷“源码剖析”。

建议命名规则

在避免冲突的情况下,函数命名要本着精简短小、望文知意的原则。

  • 通常是动词和介词加上名词,例如scanWords。
  • 避免不必要的缩写,printError要比printErr更好一些。
  • 避免使用类型关键字,比如buildUserStruct看上去会很别扭。
  • 避免歧义,不能有多种用途的解释造成误解。
  • 避免只能通过大小写区分的同名函数。
  • 避免与内置函数同名,这会导致误用。
  • 避免使用数字,除非是特定专有名词,例如UTF8。
  • 避免添加作用域提示前缀。
  • 统一使用camel/pascal case拼写风格。
  • 使用相同术语,保持一致性。
  • 使用习惯用语,比如init表示初始化,is/has返回布尔值结果。
  • 使用反义词组命名行为相反的函数,比如get/set、min/max等。

函数和方法的命名规则稍有些不同。方法通过选择符调用,且具备状态上下文,可使用更简短的动词命名。

Go 语言中 init() 函数介绍及执行顺序_golang 包的 init 函数什么时候执行